1 TCP的三次握手四次挥手

1.1 三次握手
1.2 四次挥手
挥手是任意的,客户端和服务端都可以首先断开连接
下面是以客户端发起的挥手
- 1 客户端发送完数据后发起FIN 请求,之后直接断开,
- 2 服务端返回一个ACK
- 3此时服务端还没有接收完数据,所以没有断开连接,接收完后,服务端想要断开,发送FIN
- 4 客户端返回ACK,结束
现实中的是TIME WAIT大量存在于服务端,是服务端主动断开连接,它在等待客户端发送ACK
总结
1 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的.
2 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
3 TIMEWAIT的作用?
主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。
[TCP资料](http://blog.csdn.net/u011726984/article/details/50781212
2 粘包现象
粘包存在两个方面,一个是服务端,一个是客户端
2.1 基于TCP制作远程执行命令操作(win服务端)
下面是服务端放在本地win平台,用127.0.0.1自己测试的代码
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import socket import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('127.0.0.1', 8888)) phone.listen(5) print("starting...") while True: conn, addr = phone.accept() while True: try: data = conn.recv(1024) if not data: continue res = subprocess.Popen( data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ''' # 这是判断错误的方式 err = res.stderr.read() # 首先判断错误信息 if not err: # 这是读取到的正常信息 res_cmd = res.stdout.read() else: res_cmd = err # 这是读取道的错误信息 conn.send(res_cmd) # 最后把接收到的数据发送到客户端 ''' res_err = res.stderr.read() res_out = res.stdout.read() conn.send(res_err) conn.send(res_out) except Exception: break conn.close() phone.close()
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("127.0.0.1", 8888)) while True: cmd = input(">>:").strip() if not cmd: continue phone.send(bytes(cmd.encode("utf-8"))) data = phone.recv(1024) print(data.decode('gbk')) phone.close()
|
实验的时候首先开启服务端,然后开启客户端
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| >>:cd D:\Python\...\.... >>:listen 'listen' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 >>:ls 'ls' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 >>:tree 卷 我的小D 的文件夹 PATH 列表 卷序列号为 0005-B7F0 D:. 没有子文件夹 >>:hello 'hello' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 >>:
|
2.1 基于TCP制作远程执行命令操作(Linux服务端)
在Linux上需要修改IP地址位服务端的地址,客户端的解码方式修改成utf-8
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import socket import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('192.168.16.200', 8800)) phone.listen(5) print("starting...") while True: conn, addr = phone.accept() while True: try: data = conn.recv(1024) if not data: continue res = subprocess.Popen( data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ''' # 这是判断错误的方式 err = res.stderr.read() # 首先判断错误信息 if not err: # 这是读取到的正常信息 res_cmd = res.stdout.read() else: res_cmd = err # 这是读取道的错误信息 conn.send(res_cmd) # 最后把接收到的数据发送到客户端 ''' res_err = res.stderr.read() res_out = res.stdout.read() conn.send(res_err) conn.send(res_out) except Exception: break conn.close() phone.close()
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("192.168.16.200", 8800)) while True: cmd = input(">>:").strip() if not cmd: continue phone.send(bytes(cmd.encode("utf-8"))) data = phone.recv(1024) print(data.decode('utf-8')) phone.close()
|
执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| >>:pwd / >>:cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/spool/mail:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin games:x:12:100:games:/usr/games:/sbin/nologin ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin nobody:x:99:99:Nobody:/:/sbin/nologin systemd-bus-proxy:x:999:998:systemd Bus Proxy:/:/sbin/nologin systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin dbus:x:81:81:System message bus:/:/sbin/nologin polkitd:x:998:997:User for polkitd:/:/sbin/nologin tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/dev/null:/sbin/nologin sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin postfix:x:89:89::/var/spool/postfix:/sbin/nologin hzx:x:1000:1000::/home/hzx:/bin/bash help:x:1001:1001::/home/hel >>:ls # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!看下面的就是粘包 p:/bin/bash egon:x:1002:1002::/home/egon:/bin/bash tom:x:1004:1004::/home/tom:/bin/bash jake:x:1005:1006::/home/jake:/bin/bash rose:x:1006:1007::/home/rose:/bin/bash nginx:x:997:995:Nginx web server:/var/lib/nginx:/sbin/nologin rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin >>:ls # 现在才是正常执行的结果 bin boot dev etc FtpServer hello.py home lib lib64 media mnt opt proc Python-3.6.0 Python-3.6.0.tgz root run sbin sdb1 sdb2 sdb3 server.py share srv sys tail_new.py tail.py tartmp test test3 tmp tmux tmux-2.3 tmux-2.3.tar.gz usr var >>:
|
2.2 粘包产生的原因:
客户端都是从自己的缓存中找,send后就是由操作系统取发送,不知道数据有多长
上面出现了粘包的问题,粘包产生的主要原因是TCP协议是面向流(socket.SOCK_STREAM)),意思就是说发送端可以一个字节一个字节的发送, 软件的速度是大于网络的速度(网络有延迟),TCP把数据量小,时间间隔小的进行封包,一块发送,这就无法区分包的边界
接收端从自己的缓存中是有多少取多少,发送过来的是字节是没有边界的,所以收的时候是通过修改recv(1024)中的参数来增加一次性接收的值,但是这个如果是一个大文件的时候是有问题的,会全部去取到内存中,内存会爆。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
3 解决粘包
粘包是不知道数据长度,就是没有关于数据的描述信息。1
类似以太网协议,在数据的前面加上固定长度的报头,报头中包含字节流的长度,然后一次send到对端。
对端在接收时,先从自己的缓存中取出定长的报头,然后再获取真实的数据
3.1 struck模块
struck可以把一个类型,如数字转换成bytes
1 2 3 4 5
| struct.pact('i',1234567890) ''' 结果: b'\xd2\x02\x96I' '''
|
struc
1 2 3 4 5 6 7 8 9 10
| >>> import struct >>> struct.pack('i',1231) b'\xcf\x04\x00\x00' >>> res = struct.pack('i',1231) >>> res2 = struct.unpack('i',res) >>> print(res2) (1231,) >>> res2 = struct.unpack('i',res)[0] >>> print(res2) 1231
|
准换数字的范围是: -2147483648 <= number <= 2147483647

3.2制作报头
模拟以太网协议的报头
**固定长度
描述信息**
3.2.1 win版本
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import socket import subprocess import struct import json phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('127.0.0.1', 8800)) phone.listen(5) print("starting...") while True: conn, addr = phone.accept() while True: try: data = conn.recv(1024) if not data: continue res = subprocess.Popen( data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ''' # 这是判断错误输出的方式 err = res.stderr.read() # 首先判断错误信息 if not err: # 这是读取到的正常信息 res_cmd = res.stdout.read() else: res_cmd = err # 这是读取道的错误信息 conn.send(res_cmd) # 最后把接收到的数据发送到客户端 ''' res_err = res.stderr.read() res_out = res.stdout.read() data_size = len(res_err) + len(res_out) ''' data_size = str(len(res_err) + len(res_out)) # 统计数据的长度 这种情况仍然是不行的 传输的还是没有分界 ''' conn.send(struct.pack('i', data_size)) conn.send(res_err) conn.send(res_out) except Exception: break conn.close() phone.close()
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import socket import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("127.0.0.1", 8800)) while True: cmd = input(">>:").strip() if not cmd: continue phone.send(bytes(cmd.encode("utf-8"))) gram_head = phone.recv(4) data_size = struct.unpack('i', gram_head)[0] recv_size = 0 recv_data = b'' while recv_size < data_size: data = phone.recv(1024) recv_size += len(data) recv_data += data print(recv_data.decode('gbk')) phone.close()
|
3.2.2 Linux 版本
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import socket import subprocess import struct import json phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('192.168.16.200', 8880)) phone.listen(5) print("starting...") while True: conn, addr = phone.accept() while True: try: data = conn.recv(1024) if not data: continue res = subprocess.Popen( data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ''' # 这是判断错误输出的方式 err = res.stderr.read() # 首先判断错误信息 if not err: # 这是读取到的正常信息 res_cmd = res.stdout.read() else: res_cmd = err # 这是读取道的错误信息 conn.send(res_cmd) # 最后把接收到的数据发送到客户端 ''' res_err = res.stderr.read() res_out = res.stdout.read() data_size = len(res_err) + len(res_out) ''' data_size = str(len(res_err) + len(res_out)) # 统计数据的长度 这种情况仍然是不行的 传输的还是没有分界 ''' conn.send(struct.pack('i', data_size)) conn.send(res_err) conn.send(res_out) except Exception: break conn.close() phone.close()
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import socket import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("192.168.16.200", 8880)) while True: cmd = input(">>:").strip() if not cmd: continue phone.send(bytes(cmd.encode("utf-8"))) gram_head = phone.recv(4) data_size = struct.unpack('i', gram_head)[0] recv_size = 0 recv_data = b'' while recv_size < data_size: data = phone.recv(1024) recv_size += len(data) recv_data += data print(data.decode('utf-8')) phone.close()
|
3.3 上面的问题
上面虽然解决了粘包的问题,但是在在后期传输大的文件的时候,报头的数据就会很大,也就是全部读入内存,会让内存爆。
#### 3.3.1 通过序列化解决问题
可以把报头制作成字典,字典中包含将要发送的数据的真实信息,然后把字典json序列化,然后用struct把序列化后数据打包成4个字节。
发送过程
1 先发送报头长度
2 编码报头内容然后再发送
3 最后发送真实内容
接收过程
1 接收报头的长度,用struct.unpack获得
2 取固定长度的报头(4个字节),然后解码,反序列化
3 从反序列化的字典中的寻找数据的内容,然后获取数据的真实信息
解决粘包的最终版Linux服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import socket import subprocess import json import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) phone.bind(('192.168.16.200', 8220)) phone.listen(5) print("sever is starting...") while True: conn, addr = phone.accept() print(conn) print(addr) while True: try: data = conn.recv(1024) if not data: continue res = subprocess.Popen(data.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) res_err = res.stderr.read() res_out = res.stdout.read() data_size = len(res_err) + len(res_out) head_dic = {"data_size": data_size} head_json = json.dumps(head_dic) head_bytes = head_json.encode("utf-8") head_len = len(head_bytes) conn.send(struct.pack("i", head_len)) conn.send(head_bytes) conn.send(res_err) conn.send(res_out) except Exception: break conn.close() phone.close()
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import socket import json import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("192.168.43.20", 8220)) while True: cmd = input(">>:").strip() if not cmd: continue phone.send(bytes(cmd,encoding="utf-8")) head_struct = phone.recv(4) head_len = struct.unpack("i", head_struct)[0] head_bytes = phone.recv(head_len) head_json = head_bytes.decode("utf-8") head_dic = json.loads(head_json) print(head_dic) data_size = head_dic["data_size"] recv_size = 0 recv_data = b'' while recv_size < data_size: data = phone.recv(1024) recv_size += len(data) recv_data += data print(recv_data.decode("utf-8")) phone.close()
|
Linux注意上传文件目录的权限777
4 UDP协议